1
|
|
|
/* global kirkiControlLoader */ |
2
|
|
|
var RepeaterRow = function( rowIndex, container, label, control ) { |
3
|
|
|
|
4
|
|
|
'use strict'; |
5
|
|
|
|
6
|
|
|
var self = this; |
7
|
|
|
this.rowIndex = rowIndex; |
8
|
|
|
this.container = container; |
9
|
|
|
this.label = label; |
10
|
|
|
this.header = this.container.find( '.repeater-row-header' ), |
11
|
|
|
|
12
|
|
|
this.header.on( 'click', function() { |
13
|
|
|
self.toggleMinimize(); |
14
|
|
|
}); |
15
|
|
|
|
16
|
|
|
this.container.on( 'click', '.repeater-row-remove', function() { |
17
|
|
|
self.remove(); |
18
|
|
|
}); |
19
|
|
|
|
20
|
|
|
this.header.on( 'mousedown', function() { |
21
|
|
|
self.container.trigger( 'row:start-dragging' ); |
22
|
|
|
}); |
23
|
|
|
|
24
|
|
|
this.container.on( 'keyup change', 'input, select, textarea', function( e ) { |
25
|
|
|
self.container.trigger( 'row:update', [ self.rowIndex, jQuery( e.target ).data( 'field' ), e.target ] ); |
26
|
|
|
}); |
27
|
|
|
|
28
|
|
|
this.setRowIndex = function( rowIndex ) { |
29
|
|
|
this.rowIndex = rowIndex; |
30
|
|
|
this.container.attr( 'data-row', rowIndex ); |
31
|
|
|
this.container.data( 'row', rowIndex ); |
32
|
|
|
this.updateLabel(); |
33
|
|
|
}; |
34
|
|
|
|
35
|
|
|
this.toggleMinimize = function() { |
36
|
|
|
|
37
|
|
|
// Store the previous state. |
38
|
|
|
this.container.toggleClass( 'minimized' ); |
39
|
|
|
this.header.find( '.dashicons' ).toggleClass( 'dashicons-arrow-up' ).toggleClass( 'dashicons-arrow-down' ); |
40
|
|
|
}; |
41
|
|
|
|
42
|
|
|
this.remove = function() { |
43
|
|
|
this.container.slideUp( 300, function() { |
44
|
|
|
jQuery( this ).detach(); |
45
|
|
|
}); |
46
|
|
|
this.container.trigger( 'row:remove', [ this.rowIndex ] ); |
47
|
|
|
}; |
48
|
|
|
|
49
|
|
|
this.updateLabel = function() { |
50
|
|
|
var rowLabelField, |
51
|
|
|
rowLabel, |
52
|
|
|
rowLabelSelector; |
53
|
|
|
|
54
|
|
|
if ( 'field' === this.label.type ) { |
55
|
|
|
rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' ); |
56
|
|
|
if ( _.isFunction( rowLabelField.val ) ) { |
57
|
|
|
rowLabel = rowLabelField.val(); |
58
|
|
|
if ( '' !== rowLabel ) { |
59
|
|
|
if ( ! _.isUndefined( control.params.fields[ this.label.field ] ) ) { |
60
|
|
|
if ( ! _.isUndefined( control.params.fields[ this.label.field ].type ) ) { |
61
|
|
|
if ( 'select' === control.params.fields[ this.label.field ].type ) { |
62
|
|
|
if ( ! _.isUndefined( control.params.fields[ this.label.field ].choices ) && ! _.isUndefined( control.params.fields[ this.label.field ].choices[ rowLabelField.val() ] ) ) { |
63
|
|
|
rowLabel = control.params.fields[ this.label.field ].choices[ rowLabelField.val() ]; |
64
|
|
|
} |
65
|
|
|
} else if ( 'radio' === control.params.fields[ this.label.field ].type || 'radio-image' === control.params.fields[ this.label.field ].type ) { |
66
|
|
|
rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked'; |
67
|
|
|
rowLabel = jQuery( rowLabelSelector ).val(); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
} |
71
|
|
|
this.header.find( '.repeater-row-label' ).text( rowLabel ); |
72
|
|
|
return; |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
} |
76
|
|
|
this.header.find( '.repeater-row-label' ).text( this.label.value + ' ' + ( this.rowIndex + 1 ) ); |
77
|
|
|
}; |
78
|
|
|
this.updateLabel(); |
79
|
|
|
}; |
80
|
|
|
|
81
|
|
|
wp.customize.controlConstructor.repeater = wp.customize.Control.extend({ |
82
|
|
|
|
83
|
|
|
// When we're finished loading continue processing |
84
|
|
|
ready: function() { |
85
|
|
|
|
86
|
|
|
'use strict'; |
87
|
|
|
|
88
|
|
|
var control = this; |
89
|
|
|
|
90
|
|
|
// Init the control. |
91
|
|
|
if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) { |
92
|
|
|
kirkiControlLoader( control ); |
93
|
|
|
} else { |
94
|
|
|
control.initKirkiControl(); |
95
|
|
|
} |
96
|
|
|
}, |
97
|
|
|
|
98
|
|
|
initKirkiControl: function() { |
99
|
|
|
|
100
|
|
|
'use strict'; |
101
|
|
|
|
102
|
|
|
var control = this, |
103
|
|
|
limit, |
104
|
|
|
theNewRow; |
105
|
|
|
|
106
|
|
|
// The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function) |
107
|
|
|
var settingValue = this.params.value; |
108
|
|
|
|
109
|
|
|
control.container.find( '.kirki-controls-loading-spinner' ).hide(); |
110
|
|
|
|
111
|
|
|
// The hidden field that keeps the data saved (though we never update it) |
112
|
|
|
this.settingField = this.container.find( '[data-customize-setting-link]' ).first(); |
113
|
|
|
|
114
|
|
|
// Set the field value for the first time, we'll fill it up later |
115
|
|
|
this.setValue( [], false ); |
116
|
|
|
|
117
|
|
|
// The DIV that holds all the rows |
118
|
|
|
this.repeaterFieldsContainer = this.container.find( '.repeater-fields' ).first(); |
119
|
|
|
|
120
|
|
|
// Set number of rows to 0 |
121
|
|
|
this.currentIndex = 0; |
122
|
|
|
|
123
|
|
|
// Save the rows objects |
124
|
|
|
this.rows = []; |
125
|
|
|
|
126
|
|
|
// Default limit choice |
127
|
|
|
limit = false; |
128
|
|
|
if ( ! _.isUndefined( this.params.choices.limit ) ) { |
129
|
|
|
limit = ( 0 >= this.params.choices.limit ) ? false : parseInt( this.params.choices.limit, 10 ); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
this.container.on( 'click', 'button.repeater-add', function( e ) { |
133
|
|
|
e.preventDefault(); |
134
|
|
|
if ( ! limit || control.currentIndex < limit ) { |
135
|
|
|
theNewRow = control.addRow(); |
136
|
|
|
theNewRow.toggleMinimize(); |
137
|
|
|
control.initColorPicker(); |
138
|
|
|
control.initSelect( theNewRow ); |
139
|
|
|
} else { |
140
|
|
|
jQuery( control.selector + ' .limit' ).addClass( 'highlight' ); |
141
|
|
|
} |
142
|
|
|
}); |
143
|
|
|
|
144
|
|
|
this.container.on( 'click', '.repeater-row-remove', function() { |
145
|
|
|
control.currentIndex--; |
146
|
|
|
if ( ! limit || control.currentIndex < limit ) { |
147
|
|
|
jQuery( control.selector + ' .limit' ).removeClass( 'highlight' ); |
148
|
|
|
} |
149
|
|
|
}); |
150
|
|
|
|
151
|
|
|
this.container.on( 'click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function( e ) { |
152
|
|
|
e.preventDefault(); |
153
|
|
|
control.$thisButton = jQuery( this ); |
154
|
|
|
control.openFrame( e ); |
155
|
|
|
}); |
156
|
|
|
|
157
|
|
|
this.container.on( 'click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function( e ) { |
158
|
|
|
e.preventDefault(); |
159
|
|
|
control.$thisButton = jQuery( this ); |
160
|
|
|
control.removeImage( e ); |
161
|
|
|
}); |
162
|
|
|
|
163
|
|
|
this.container.on( 'click keypress', '.repeater-field-upload .remove-button', function( e ) { |
164
|
|
|
e.preventDefault(); |
165
|
|
|
control.$thisButton = jQuery( this ); |
166
|
|
|
control.removeFile( e ); |
167
|
|
|
}); |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Function that loads the Mustache template |
171
|
|
|
*/ |
172
|
|
|
this.repeaterTemplate = _.memoize( function() { |
173
|
|
|
var compiled, |
174
|
|
|
/* |
175
|
|
|
* Underscore's default ERB-style templates are incompatible with PHP |
176
|
|
|
* when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax. |
177
|
|
|
* |
178
|
|
|
* @see trac ticket #22344. |
179
|
|
|
*/ |
180
|
|
|
options = { |
181
|
|
|
evaluate: /<#([\s\S]+?)#>/g, |
182
|
|
|
interpolate: /\{\{\{([\s\S]+?)\}\}\}/g, |
183
|
|
|
escape: /\{\{([^\}]+?)\}\}(?!\})/g, |
184
|
|
|
variable: 'data' |
185
|
|
|
}; |
186
|
|
|
|
187
|
|
|
return function( data ) { |
188
|
|
|
compiled = _.template( control.container.find( '.customize-control-repeater-content' ).first().html(), null, options ); |
189
|
|
|
return compiled( data ); |
190
|
|
|
}; |
191
|
|
|
}); |
192
|
|
|
|
193
|
|
|
// When we load the control, the fields have not been filled up |
194
|
|
|
// This is the first time that we create all the rows |
195
|
|
|
if ( settingValue.length ) { |
196
|
|
|
_.each( settingValue, function( subValue ) { |
197
|
|
|
theNewRow = control.addRow( subValue ); |
198
|
|
|
control.initColorPicker(); |
199
|
|
|
control.initSelect( theNewRow, subValue ); |
200
|
|
|
}); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
// Once we have displayed the rows, we cleanup the values |
204
|
|
|
this.setValue( settingValue, true, true ); |
205
|
|
|
|
206
|
|
|
this.repeaterFieldsContainer.sortable({ |
207
|
|
|
handle: '.repeater-row-header', |
208
|
|
|
update: function() { |
209
|
|
|
control.sort(); |
210
|
|
|
} |
211
|
|
|
}); |
212
|
|
|
|
213
|
|
|
}, |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Open the media modal. |
217
|
|
|
*/ |
218
|
|
|
openFrame: function( event ) { |
219
|
|
|
|
220
|
|
|
'use strict'; |
221
|
|
|
|
222
|
|
|
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) { |
223
|
|
|
return; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-cropped_image' ) ) { |
227
|
|
|
this.initCropperFrame(); |
228
|
|
|
} else { |
229
|
|
|
this.initFrame(); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
this.frame.open(); |
233
|
|
|
}, |
234
|
|
|
|
235
|
|
|
initFrame: function() { |
236
|
|
|
|
237
|
|
|
'use strict'; |
238
|
|
|
|
239
|
|
|
var libMediaType = this.getMimeType(); |
240
|
|
|
|
241
|
|
|
this.frame = wp.media({ |
242
|
|
|
states: [ |
243
|
|
|
new wp.media.controller.Library({ |
244
|
|
|
library: wp.media.query({ type: libMediaType }), |
245
|
|
|
multiple: false, |
246
|
|
|
date: false |
247
|
|
|
}) |
248
|
|
|
] |
249
|
|
|
}); |
250
|
|
|
|
251
|
|
|
// When a file is selected, run a callback. |
252
|
|
|
this.frame.on( 'select', this.onSelect, this ); |
253
|
|
|
}, |
254
|
|
|
/** |
255
|
|
|
* Create a media modal select frame, and store it so the instance can be reused when needed. |
256
|
|
|
* This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js |
257
|
|
|
*/ |
258
|
|
|
initCropperFrame: function() { |
259
|
|
|
|
260
|
|
|
'use strict'; |
261
|
|
|
|
262
|
|
|
// We get the field id from which this was called |
263
|
|
|
var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ), |
264
|
|
|
attrs = [ 'width', 'height', 'flex_width', 'flex_height' ], // A list of attributes to look for |
265
|
|
|
libMediaType = this.getMimeType(); |
266
|
|
|
|
267
|
|
|
// Make sure we got it |
268
|
|
|
if ( _.isString( currentFieldId ) && '' !== currentFieldId ) { |
269
|
|
|
|
270
|
|
|
// Make fields is defined and only do the hack for cropped_image |
271
|
|
|
if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'cropped_image' === this.params.fields[ currentFieldId ].type ) { |
272
|
|
|
|
273
|
|
|
//Iterate over the list of attributes |
274
|
|
|
attrs.forEach( function( el ) { |
275
|
|
|
|
276
|
|
|
// If the attribute exists in the field |
277
|
|
|
if ( ! _.isUndefined( this.params.fields[ currentFieldId ][ el ] ) ) { |
278
|
|
|
|
279
|
|
|
// Set the attribute in the main object |
280
|
|
|
this.params[ el ] = this.params.fields[ currentFieldId ][ el ]; |
281
|
|
|
} |
282
|
|
|
}.bind( this ) ); |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
this.frame = wp.media({ |
287
|
|
|
button: { |
288
|
|
|
text: 'Select and Crop', |
289
|
|
|
close: false |
290
|
|
|
}, |
291
|
|
|
states: [ |
292
|
|
|
new wp.media.controller.Library({ |
293
|
|
|
library: wp.media.query({ type: libMediaType }), |
294
|
|
|
multiple: false, |
295
|
|
|
date: false, |
296
|
|
|
suggestedWidth: this.params.width, |
297
|
|
|
suggestedHeight: this.params.height |
298
|
|
|
}), |
299
|
|
|
new wp.media.controller.CustomizeImageCropper({ |
300
|
|
|
imgSelectOptions: this.calculateImageSelectOptions, |
301
|
|
|
control: this |
302
|
|
|
}) |
303
|
|
|
] |
304
|
|
|
}); |
305
|
|
|
|
306
|
|
|
this.frame.on( 'select', this.onSelectForCrop, this ); |
307
|
|
|
this.frame.on( 'cropped', this.onCropped, this ); |
308
|
|
|
this.frame.on( 'skippedcrop', this.onSkippedCrop, this ); |
309
|
|
|
|
310
|
|
|
}, |
311
|
|
|
|
312
|
|
|
onSelect: function() { |
313
|
|
|
|
314
|
|
|
'use strict'; |
315
|
|
|
|
316
|
|
|
var attachment = this.frame.state().get( 'selection' ).first().toJSON(); |
317
|
|
|
|
318
|
|
|
if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-upload' ) ) { |
319
|
|
|
this.setFileInRepeaterField( attachment ); |
320
|
|
|
} else { |
321
|
|
|
this.setImageInRepeaterField( attachment ); |
322
|
|
|
} |
323
|
|
|
}, |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* After an image is selected in the media modal, switch to the cropper |
327
|
|
|
* state if the image isn't the right size. |
328
|
|
|
*/ |
329
|
|
|
|
330
|
|
|
onSelectForCrop: function() { |
331
|
|
|
|
332
|
|
|
'use strict'; |
333
|
|
|
|
334
|
|
|
var attachment = this.frame.state().get( 'selection' ).first().toJSON(); |
335
|
|
|
|
336
|
|
|
if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) { |
337
|
|
|
this.setImageInRepeaterField( attachment ); |
338
|
|
|
} else { |
339
|
|
|
this.frame.setState( 'cropper' ); |
340
|
|
|
} |
341
|
|
|
}, |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* After the image has been cropped, apply the cropped image data to the setting. |
345
|
|
|
* |
346
|
|
|
* @param {object} croppedImage Cropped attachment data. |
347
|
|
|
*/ |
348
|
|
|
onCropped: function( croppedImage ) { |
349
|
|
|
|
350
|
|
|
'use strict'; |
351
|
|
|
|
352
|
|
|
this.setImageInRepeaterField( croppedImage ); |
353
|
|
|
|
354
|
|
|
}, |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Returns a set of options, computed from the attached image data and |
358
|
|
|
* control-specific data, to be fed to the imgAreaSelect plugin in |
359
|
|
|
* wp.media.view.Cropper. |
360
|
|
|
* |
361
|
|
|
* @param {wp.media.model.Attachment} attachment |
362
|
|
|
* @param {wp.media.controller.Cropper} controller |
363
|
|
|
* @returns {Object} Options |
364
|
|
|
*/ |
365
|
|
|
calculateImageSelectOptions: function( attachment, controller ) { |
366
|
|
|
|
367
|
|
|
'use strict'; |
368
|
|
|
|
369
|
|
|
var control = controller.get( 'control' ), |
370
|
|
|
flexWidth = !! parseInt( control.params.flex_width, 10 ), |
371
|
|
|
flexHeight = !! parseInt( control.params.flex_height, 10 ), |
372
|
|
|
realWidth = attachment.get( 'width' ), |
373
|
|
|
realHeight = attachment.get( 'height' ), |
374
|
|
|
xInit = parseInt( control.params.width, 10 ), |
375
|
|
|
yInit = parseInt( control.params.height, 10 ), |
376
|
|
|
ratio = xInit / yInit, |
377
|
|
|
xImg = realWidth, |
378
|
|
|
yImg = realHeight, |
379
|
|
|
x1, |
380
|
|
|
y1, |
381
|
|
|
imgSelectOptions; |
382
|
|
|
|
383
|
|
|
controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) ); |
384
|
|
|
|
385
|
|
|
if ( xImg / yImg > ratio ) { |
386
|
|
|
yInit = yImg; |
387
|
|
|
xInit = yInit * ratio; |
388
|
|
|
} else { |
389
|
|
|
xInit = xImg; |
390
|
|
|
yInit = xInit / ratio; |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
x1 = ( xImg - xInit ) / 2; |
394
|
|
|
y1 = ( yImg - yInit ) / 2; |
395
|
|
|
|
396
|
|
|
imgSelectOptions = { |
397
|
|
|
handles: true, |
398
|
|
|
keys: true, |
399
|
|
|
instance: true, |
400
|
|
|
persistent: true, |
401
|
|
|
imageWidth: realWidth, |
402
|
|
|
imageHeight: realHeight, |
403
|
|
|
x1: x1, |
404
|
|
|
y1: y1, |
405
|
|
|
x2: xInit + x1, |
406
|
|
|
y2: yInit + y1 |
407
|
|
|
}; |
408
|
|
|
|
409
|
|
|
if ( false === flexHeight && false === flexWidth ) { |
410
|
|
|
imgSelectOptions.aspectRatio = xInit + ':' + yInit; |
411
|
|
|
} |
412
|
|
|
if ( false === flexHeight ) { |
413
|
|
|
imgSelectOptions.maxHeight = yInit; |
414
|
|
|
} |
415
|
|
|
if ( false === flexWidth ) { |
416
|
|
|
imgSelectOptions.maxWidth = xInit; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
return imgSelectOptions; |
420
|
|
|
}, |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Return whether the image must be cropped, based on required dimensions. |
424
|
|
|
* |
425
|
|
|
* @param {bool} flexW |
426
|
|
|
* @param {bool} flexH |
427
|
|
|
* @param {int} dstW |
428
|
|
|
* @param {int} dstH |
429
|
|
|
* @param {int} imgW |
430
|
|
|
* @param {int} imgH |
431
|
|
|
* @return {bool} |
432
|
|
|
*/ |
433
|
|
|
mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) { |
434
|
|
|
|
435
|
|
|
'use strict'; |
436
|
|
|
|
437
|
|
|
if ( ( true === flexW && true === flexH ) || ( true === flexW && dstH === imgH ) || ( true === flexH && dstW === imgW ) || ( dstW === imgW && dstH === imgH ) || ( imgW <= dstW ) ) { |
438
|
|
|
return false; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
return true; |
442
|
|
|
}, |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* If cropping was skipped, apply the image data directly to the setting. |
446
|
|
|
*/ |
447
|
|
|
onSkippedCrop: function() { |
448
|
|
|
|
449
|
|
|
'use strict'; |
450
|
|
|
|
451
|
|
|
var attachment = this.frame.state().get( 'selection' ).first().toJSON(); |
452
|
|
|
this.setImageInRepeaterField( attachment ); |
453
|
|
|
|
454
|
|
|
}, |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* Updates the setting and re-renders the control UI. |
458
|
|
|
* |
459
|
|
|
* @param {object} attachment |
460
|
|
|
*/ |
461
|
|
|
setImageInRepeaterField: function( attachment ) { |
462
|
|
|
|
463
|
|
|
'use strict'; |
464
|
|
|
|
465
|
|
|
var $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image' ); |
466
|
|
|
|
467
|
|
|
$targetDiv.find( '.kirki-image-attachment' ).html( '<img src="' + attachment.url + '">' ).hide().slideDown( 'slow' ); |
468
|
|
|
|
469
|
|
|
$targetDiv.find( '.hidden-field' ).val( attachment.id ); |
470
|
|
|
this.$thisButton.text( this.$thisButton.data( 'alt-label' ) ); |
471
|
|
|
$targetDiv.find( '.remove-button' ).show(); |
472
|
|
|
|
473
|
|
|
//This will activate the save button |
474
|
|
|
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' ); |
475
|
|
|
this.frame.close(); |
476
|
|
|
|
477
|
|
|
}, |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Updates the setting and re-renders the control UI. |
481
|
|
|
* |
482
|
|
|
* @param {object} attachment |
483
|
|
|
*/ |
484
|
|
|
setFileInRepeaterField: function( attachment ) { |
485
|
|
|
|
486
|
|
|
'use strict'; |
487
|
|
|
|
488
|
|
|
var $targetDiv = this.$thisButton.closest( '.repeater-field-upload' ); |
489
|
|
|
|
490
|
|
|
$targetDiv.find( '.kirki-file-attachment' ).html( '<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>' ).hide().slideDown( 'slow' ); |
491
|
|
|
|
492
|
|
|
$targetDiv.find( '.hidden-field' ).val( attachment.id ); |
493
|
|
|
this.$thisButton.text( this.$thisButton.data( 'alt-label' ) ); |
494
|
|
|
$targetDiv.find( '.upload-button' ).show(); |
495
|
|
|
$targetDiv.find( '.remove-button' ).show(); |
496
|
|
|
|
497
|
|
|
//This will activate the save button |
498
|
|
|
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' ); |
499
|
|
|
this.frame.close(); |
500
|
|
|
|
501
|
|
|
}, |
502
|
|
|
|
503
|
|
|
getMimeType: function() { |
504
|
|
|
|
505
|
|
|
'use strict'; |
506
|
|
|
|
507
|
|
|
// We get the field id from which this was called |
508
|
|
|
var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ); |
509
|
|
|
|
510
|
|
|
// Make sure we got it |
511
|
|
|
if ( _.isString( currentFieldId ) && '' !== currentFieldId ) { |
512
|
|
|
|
513
|
|
|
// Make fields is defined and only do the hack for cropped_image |
514
|
|
|
if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'upload' === this.params.fields[ currentFieldId ].type ) { |
515
|
|
|
|
516
|
|
|
// If the attribute exists in the field |
517
|
|
|
if ( ! _.isUndefined( this.params.fields[ currentFieldId ].mime_type ) ) { |
518
|
|
|
|
519
|
|
|
// Set the attribute in the main object |
520
|
|
|
return this.params.fields[ currentFieldId ].mime_type; |
521
|
|
|
} |
522
|
|
|
} |
523
|
|
|
} |
524
|
|
|
return 'image'; |
525
|
|
|
|
526
|
|
|
}, |
527
|
|
|
|
528
|
|
|
removeImage: function( event ) { |
529
|
|
|
|
530
|
|
|
'use strict'; |
531
|
|
|
|
532
|
|
|
var $targetDiv, |
533
|
|
|
$uploadButton; |
534
|
|
|
|
535
|
|
|
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) { |
536
|
|
|
return; |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
$targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload' ); |
540
|
|
|
$uploadButton = $targetDiv.find( '.upload-button' ); |
541
|
|
|
|
542
|
|
|
$targetDiv.find( '.kirki-image-attachment' ).slideUp( 'fast', function() { |
543
|
|
|
jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) ); |
544
|
|
|
}); |
545
|
|
|
$targetDiv.find( '.hidden-field' ).val( '' ); |
546
|
|
|
$uploadButton.text( $uploadButton.data( 'label' ) ); |
547
|
|
|
this.$thisButton.hide(); |
548
|
|
|
|
549
|
|
|
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' ); |
550
|
|
|
|
551
|
|
|
}, |
552
|
|
|
|
553
|
|
|
removeFile: function( event ) { |
554
|
|
|
|
555
|
|
|
'use strict'; |
556
|
|
|
|
557
|
|
|
var $targetDiv, |
558
|
|
|
$uploadButton; |
559
|
|
|
|
560
|
|
|
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) { |
561
|
|
|
return; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
$targetDiv = this.$thisButton.closest( '.repeater-field-upload' ); |
565
|
|
|
$uploadButton = $targetDiv.find( '.upload-button' ); |
566
|
|
|
|
567
|
|
|
$targetDiv.find( '.kirki-file-attachment' ).slideUp( 'fast', function() { |
568
|
|
|
jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) ); |
569
|
|
|
}); |
570
|
|
|
$targetDiv.find( '.hidden-field' ).val( '' ); |
571
|
|
|
$uploadButton.text( $uploadButton.data( 'label' ) ); |
572
|
|
|
this.$thisButton.hide(); |
573
|
|
|
|
574
|
|
|
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' ); |
575
|
|
|
|
576
|
|
|
}, |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Get the current value of the setting |
580
|
|
|
* |
581
|
|
|
* @return Object |
582
|
|
|
*/ |
583
|
|
|
getValue: function() { |
584
|
|
|
|
585
|
|
|
'use strict'; |
586
|
|
|
|
587
|
|
|
// The setting is saved in JSON |
588
|
|
|
return JSON.parse( decodeURI( this.setting.get() ) ); |
589
|
|
|
|
590
|
|
|
}, |
591
|
|
|
|
592
|
|
|
/** |
593
|
|
|
* Set a new value for the setting |
594
|
|
|
* |
595
|
|
|
* @param newValue Object |
596
|
|
|
* @param refresh If we want to refresh the previewer or not |
597
|
|
|
*/ |
598
|
|
|
setValue: function( newValue, refresh, filtering ) { |
599
|
|
|
|
600
|
|
|
'use strict'; |
601
|
|
|
|
602
|
|
|
// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB |
603
|
|
|
var filteredValue = newValue, |
604
|
|
|
filter = []; |
605
|
|
|
|
606
|
|
|
if ( filtering ) { |
607
|
|
|
jQuery.each( this.params.fields, function( index, value ) { |
608
|
|
|
if ( 'image' === value.type || 'cropped_image' === value.type || 'upload' === value.type ) { |
609
|
|
|
filter.push( index ); |
610
|
|
|
} |
611
|
|
|
}); |
612
|
|
|
jQuery.each( newValue, function( index, value ) { |
613
|
|
|
jQuery.each( filter, function( ind, field ) { |
614
|
|
|
if ( ! _.isUndefined( value[ field ] ) && ! _.isUndefined( value[ field ].id ) ) { |
615
|
|
|
filteredValue[index][ field ] = value[ field ].id; |
616
|
|
|
} |
617
|
|
|
}); |
618
|
|
|
}); |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
this.setting.set( encodeURI( JSON.stringify( filteredValue ) ) ); |
622
|
|
|
|
623
|
|
|
if ( refresh ) { |
624
|
|
|
|
625
|
|
|
// Trigger the change event on the hidden field so |
626
|
|
|
// previewer refresh the website on Customizer |
627
|
|
|
this.settingField.trigger( 'change' ); |
628
|
|
|
} |
629
|
|
|
}, |
630
|
|
|
|
631
|
|
|
/** |
632
|
|
|
* Add a new row to repeater settings based on the structure. |
633
|
|
|
* |
634
|
|
|
* @param data (Optional) Object of field => value pairs (undefined if you want to get the default values) |
635
|
|
|
*/ |
636
|
|
|
addRow: function( data ) { |
637
|
|
|
|
638
|
|
|
'use strict'; |
639
|
|
|
|
640
|
|
|
var control = this, |
641
|
|
|
template = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ). |
642
|
|
|
settingValue = this.getValue(), // Get the current setting value. |
643
|
|
|
newRowSetting = {}, // Saves the new setting data. |
644
|
|
|
templateData, // Data to pass to the template |
645
|
|
|
newRow, |
646
|
|
|
i; |
647
|
|
|
|
648
|
|
|
if ( template ) { |
649
|
|
|
|
650
|
|
|
// The control structure is going to define the new fields |
651
|
|
|
// We need to clone control.params.fields. Assigning it |
652
|
|
|
// ould result in a reference assignment. |
653
|
|
|
templateData = jQuery.extend( true, {}, control.params.fields ); |
654
|
|
|
|
655
|
|
|
// But if we have passed data, we'll use the data values instead |
656
|
|
|
if ( data ) { |
657
|
|
|
for ( i in data ) { |
658
|
|
|
if ( data.hasOwnProperty( i ) && templateData.hasOwnProperty( i ) ) { |
659
|
|
|
templateData[ i ]['default'] = data[ i ]; |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
templateData.index = this.currentIndex; |
665
|
|
|
|
666
|
|
|
// Append the template content |
667
|
|
|
template = template( templateData ); |
668
|
|
|
|
669
|
|
|
// Create a new row object and append the element |
670
|
|
|
newRow = new RepeaterRow( |
671
|
|
|
control.currentIndex, |
672
|
|
|
jQuery( template ).appendTo( control.repeaterFieldsContainer ), |
673
|
|
|
control.params.row_label, |
674
|
|
|
control |
675
|
|
|
); |
676
|
|
|
|
677
|
|
|
newRow.container.on( 'row:remove', function( e, rowIndex ) { |
678
|
|
|
control.deleteRow( rowIndex ); |
679
|
|
|
}); |
680
|
|
|
|
681
|
|
|
newRow.container.on( 'row:update', function( e, rowIndex, fieldName, element ) { |
682
|
|
|
control.updateField.call( control, e, rowIndex, fieldName, element ); |
683
|
|
|
newRow.updateLabel(); |
684
|
|
|
}); |
685
|
|
|
|
686
|
|
|
// Add the row to rows collection |
687
|
|
|
this.rows[ this.currentIndex ] = newRow; |
688
|
|
|
|
689
|
|
|
for ( i in templateData ) { |
690
|
|
|
if ( templateData.hasOwnProperty( i ) ) { |
691
|
|
|
newRowSetting[ i ] = templateData[ i ]['default']; |
692
|
|
|
} |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
settingValue[ this.currentIndex ] = newRowSetting; |
696
|
|
|
this.setValue( settingValue, true ); |
697
|
|
|
|
698
|
|
|
this.currentIndex++; |
699
|
|
|
|
700
|
|
|
return newRow; |
701
|
|
|
} |
702
|
|
|
}, |
703
|
|
|
|
704
|
|
|
sort: function() { |
705
|
|
|
|
706
|
|
|
'use strict'; |
707
|
|
|
|
708
|
|
|
var control = this, |
709
|
|
|
$rows = this.repeaterFieldsContainer.find( '.repeater-row' ), |
710
|
|
|
newOrder = [], |
711
|
|
|
settings = control.getValue(), |
712
|
|
|
newRows = [], |
713
|
|
|
newSettings = []; |
714
|
|
|
|
715
|
|
|
$rows.each( function( i, element ) { |
716
|
|
|
newOrder.push( jQuery( element ).data( 'row' ) ); |
717
|
|
|
}); |
718
|
|
|
|
719
|
|
|
jQuery.each( newOrder, function( newPosition, oldPosition ) { |
720
|
|
|
newRows[ newPosition ] = control.rows[ oldPosition ]; |
721
|
|
|
newRows[ newPosition ].setRowIndex( newPosition ); |
722
|
|
|
|
723
|
|
|
newSettings[ newPosition ] = settings[ oldPosition ]; |
724
|
|
|
}); |
725
|
|
|
|
726
|
|
|
control.rows = newRows; |
727
|
|
|
control.setValue( newSettings ); |
728
|
|
|
|
729
|
|
|
}, |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Delete a row in the repeater setting |
733
|
|
|
* |
734
|
|
|
* @param index Position of the row in the complete Setting Array |
735
|
|
|
*/ |
736
|
|
|
deleteRow: function( index ) { |
737
|
|
|
|
738
|
|
|
'use strict'; |
739
|
|
|
|
740
|
|
|
var currentSettings = this.getValue(), |
741
|
|
|
row, |
742
|
|
|
i, |
743
|
|
|
prop; |
744
|
|
|
|
745
|
|
|
if ( currentSettings[ index ] ) { |
746
|
|
|
|
747
|
|
|
// Find the row |
748
|
|
|
row = this.rows[ index ]; |
749
|
|
|
if ( row ) { |
750
|
|
|
|
751
|
|
|
// Remove the row settings |
752
|
|
|
delete currentSettings[ index ]; |
753
|
|
|
|
754
|
|
|
// Remove the row from the rows collection |
755
|
|
|
delete this.rows[ index ]; |
756
|
|
|
|
757
|
|
|
// Update the new setting values |
758
|
|
|
this.setValue( currentSettings, true ); |
759
|
|
|
|
760
|
|
|
} |
761
|
|
|
|
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
// Remap the row numbers |
765
|
|
|
i = 1; |
766
|
|
|
for ( prop in this.rows ) { |
767
|
|
|
if ( this.rows.hasOwnProperty( prop ) && this.rows[ prop ] ) { |
768
|
|
|
this.rows[ prop ].updateLabel(); |
769
|
|
|
i++; |
770
|
|
|
} |
771
|
|
|
} |
772
|
|
|
}, |
773
|
|
|
|
774
|
|
|
/** |
775
|
|
|
* Update a single field inside a row. |
776
|
|
|
* Triggered when a field has changed |
777
|
|
|
* |
778
|
|
|
* @param e Event Object |
779
|
|
|
*/ |
780
|
|
|
updateField: function( e, rowIndex, fieldId, element ) { |
781
|
|
|
|
782
|
|
|
'use strict'; |
783
|
|
|
|
784
|
|
|
var type, |
785
|
|
|
row, |
786
|
|
|
currentSettings; |
787
|
|
|
|
788
|
|
|
if ( ! this.rows[ rowIndex ] ) { |
789
|
|
|
return; |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
if ( ! this.params.fields[ fieldId ] ) { |
793
|
|
|
return; |
794
|
|
|
} |
795
|
|
|
|
796
|
|
|
type = this.params.fields[ fieldId].type; |
797
|
|
|
row = this.rows[ rowIndex ]; |
798
|
|
|
currentSettings = this.getValue(); |
799
|
|
|
|
800
|
|
|
element = jQuery( element ); |
801
|
|
|
|
802
|
|
|
if ( _.isUndefined( currentSettings[ row.rowIndex ][ fieldId ] ) ) { |
803
|
|
|
return; |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
if ( 'checkbox' === type ) { |
807
|
|
|
currentSettings[ row.rowIndex ][ fieldId ] = element.is( ':checked' ); |
808
|
|
|
} else { |
809
|
|
|
|
810
|
|
|
// Update the settings |
811
|
|
|
currentSettings[ row.rowIndex ][ fieldId ] = element.val(); |
812
|
|
|
} |
813
|
|
|
this.setValue( currentSettings, true ); |
814
|
|
|
}, |
815
|
|
|
|
816
|
|
|
/** |
817
|
|
|
* Init the color picker on color fields |
818
|
|
|
* Called after AddRow |
819
|
|
|
* |
820
|
|
|
*/ |
821
|
|
|
initColorPicker: function() { |
822
|
|
|
|
823
|
|
|
'use strict'; |
824
|
|
|
|
825
|
|
|
var control = this, |
826
|
|
|
colorPicker = control.container.find( '.color-picker-hex' ), |
827
|
|
|
options = {}, |
828
|
|
|
fieldId = colorPicker.data( 'field' ); |
829
|
|
|
|
830
|
|
|
// We check if the color palette parameter is defined. |
831
|
|
|
if ( ! _.isUndefined( fieldId ) && ! _.isUndefined( control.params.fields[ fieldId ] ) && ! _.isUndefined( control.params.fields[ fieldId ].palettes ) && _.isObject( control.params.fields[ fieldId ].palettes ) ) { |
832
|
|
|
options.palettes = control.params.fields[ fieldId ].palettes; |
833
|
|
|
} |
834
|
|
|
|
835
|
|
|
// When the color picker value is changed we update the value of the field |
836
|
|
|
options.change = function( event, ui ) { |
837
|
|
|
|
838
|
|
|
var currentPicker = jQuery( event.target ), |
839
|
|
|
row = currentPicker.closest( '.repeater-row' ), |
840
|
|
|
rowIndex = row.data( 'row' ), |
841
|
|
|
currentSettings = control.getValue(); |
842
|
|
|
|
843
|
|
|
currentSettings[ rowIndex ][ currentPicker.data( 'field' ) ] = ui.color.toString(); |
844
|
|
|
control.setValue( currentSettings, true ); |
845
|
|
|
|
846
|
|
|
}; |
847
|
|
|
|
848
|
|
|
// Init the color picker |
849
|
|
|
if ( 0 !== colorPicker.length ) { |
850
|
|
|
colorPicker.wpColorPicker( options ); |
851
|
|
|
} |
852
|
|
|
}, |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* Init the dropdown-pages field with selectWoo |
856
|
|
|
* Called after AddRow |
857
|
|
|
* |
858
|
|
|
* @param {object} theNewRow the row that was added to the repeater |
859
|
|
|
* @param {object} data the data for the row if we're initializing a pre-existing row |
860
|
|
|
* |
861
|
|
|
*/ |
862
|
|
|
initSelect: function( theNewRow, data ) { |
863
|
|
|
|
864
|
|
|
'use strict'; |
865
|
|
|
|
866
|
|
|
var control = this, |
867
|
|
|
dropdown = theNewRow.container.find( '.repeater-field select' ), |
868
|
|
|
$select, |
869
|
|
|
dataField, |
870
|
|
|
multiple, |
871
|
|
|
selectWooOptions = {}; |
872
|
|
|
|
873
|
|
|
if ( 0 === dropdown.length ) { |
874
|
|
|
return; |
875
|
|
|
} |
876
|
|
|
|
877
|
|
|
dataField = dropdown.data( 'field' ); |
878
|
|
|
multiple = jQuery( dropdown ).data( 'multiple' ); |
879
|
|
|
if ( 'undefed' !== multiple && jQuery.isNumeric( multiple ) ) { |
880
|
|
|
multiple = parseInt( multiple, 10 ); |
881
|
|
|
if ( 1 < multiple ) { |
882
|
|
|
selectWooOptions.maximumSelectionLength = multiple; |
883
|
|
|
} |
884
|
|
|
} |
885
|
|
|
|
886
|
|
|
data = data || {}; |
887
|
|
|
data[ dataField ] = data[ dataField ] || ''; |
888
|
|
|
|
889
|
|
|
$select = jQuery( dropdown ).selectWoo( selectWooOptions ).val( data[ dataField ] ); |
890
|
|
|
|
891
|
|
|
this.container.on( 'change', '.repeater-field select', function( event ) { |
892
|
|
|
|
893
|
|
|
var currentDropdown = jQuery( event.target ), |
894
|
|
|
row = currentDropdown.closest( '.repeater-row' ), |
895
|
|
|
rowIndex = row.data( 'row' ), |
896
|
|
|
currentSettings = control.getValue(); |
897
|
|
|
|
898
|
|
|
currentSettings[ rowIndex ][ currentDropdown.data( 'field' ) ] = jQuery( this ).val(); |
899
|
|
|
control.setValue( currentSettings ); |
900
|
|
|
}); |
901
|
|
|
} |
902
|
|
|
}); |
903
|
|
|
|